Saltar al contenido principal

Pruebas automatizadas

La automatización de pruebas es una manera eficiente de validar que el código de la aplicación funciona como se pretende. Mientras Electron no mantiene activamente su propia solución de pruebas, esta guía recorrerá un par de maneras en que puedes ejecutar pruebas automatizadas de extremo a extremo en tu aplicación Electron.

Usando la interfaz WebDriver

Para ChromeDriver - WebDriver para Chrome:

WebDriver es una herramienta de código abierto para pruebas automatizadas de aplicaciones web en varios navegadores. Provee la capacidad de navegar por páginas web, sistema de usuarios, ejecución de JavaScript, y más. ChromeDriver es un servidor independiente que implementa el protocolo de cable de WebDriver para Chromium. Ha sido desarrollado por miembros de los equipos de Chromium y WebDriver.

Hay algunas maneras en que puedes configurar las pruebas usando WebDriver.

Con WebdriverIO

WebdriverIO (WDIO) es un framework para automatización de pruebas que provee un paquete Node.js para pruebas con WebDriver. Su ecosistema además incluye varios complementos (p. ej. reportes y servicios) que pueden ayudarte a armar su configuración de prueba.

If you already have an existing WebdriverIO setup, it is recommended to update your dependencies and validate your existing configuration with how it is outlined in the docs.

Install the test runner

If you don't use WebdriverIO in your project yet, you can add it by running the starter toolkit in your project root directory:

npm init wdio@latest ./

This starts a configuration wizard that helps you put together the right setup, installs all necessary packages, and generates a wdio.conf.js configuration file. Make sure to select "Desktop Testing - of Electron Applications" on one of the first questions asking "What type of testing would you like to do?".

Conecta WDIO a tu aplicación Electron

After running the configuration wizard, your wdio.conf.js should include roughly the following content:

wdio.conf.js
export const config = {
// ...
services: ['electron'],
capabilities: [{
browserName: 'electron',
'wdio:electronServiceOptions': {
// WebdriverIO can automatically find your bundled application
// if you use Electron Forge or electron-builder, otherwise you
// can define it here, e.g.:
// appBinaryPath: './path/to/bundled/application.exe',
appArgs: ['foo', 'bar=baz']
}
}]
// ...
}

Escribe tus tests

Use the WebdriverIO API to interact with elements on the screen. The framework provides custom "matchers" that make asserting the state of your application easy, e.g.:

import { browser, $, expect } from '@wdio/globals'

describe('keyboard input', () => {
it('should detect keyboard input', async () => {
await browser.keys(['y', 'o'])
await expect($('keypress-count')).toHaveText('YO')
})
})

Furthermore, WebdriverIO allows you to access Electron APIs to get static information about your application:

import { browser, $, expect } from '@wdio/globals'

describe('when the make smaller button is clicked', () => {
it('should decrease the window height and width by 10 pixels', async () => {
const boundsBefore = await browser.electron.browserWindow('getBounds')
expect(boundsBefore.width).toEqual(210)
expect(boundsBefore.height).toEqual(310)

await $('.make-smaller').click()
const boundsAfter = await browser.electron.browserWindow('getBounds')
expect(boundsAfter.width).toEqual(200)
expect(boundsAfter.height).toEqual(300)
})
})

or to retrieve other Electron process information:

import fs from 'node:fs'
import path from 'node:path'
import { browser, expect } from '@wdio/globals'

const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), { encoding: 'utf-8' }))
const { name, version } = packageJson

describe('electron APIs', () => {
it('should retrieve app metadata through the electron API', async () => {
const appName = await browser.electron.app('getName')
expect(appName).toEqual(name)
const appVersion = await browser.electron.app('getVersion')
expect(appVersion).toEqual(version)
})

it('should pass args through to the launched application', async () => {
// custom args are set in the wdio.conf.js file as they need to be set before WDIO starts
const argv = await browser.electron.mainProcess('argv')
expect(argv).toContain('--foo')
expect(argv).toContain('--bar=baz')
})
})

Ejecutar tus pruebas

Para ejecutar tus pruebas:

$ npx wdio run wdio.conf.js

WebdriverIO helps launch and shut down the application for you.

More documentation

Find more documentation on Mocking Electron APIs and other useful resources in the official WebdriverIO documentation.

Con Selenium

Selenium es un framework de automatización web que expone enlaces a las APIs de WebDriver en muchos lenguajes. Sus enlaces Node.js están disponibles bajo el paquete selenium-webdriver en NPM.

Ejecurar un servidor ChromeDriver

Para usar Selenium con Electron, necesitas descargar y ejecutar el binario electron-chromedriver:

npm install --save-dev electron-chromedriver
./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.

Remember the port number 9515, which will be used later.

Conectar Selenium a ChromeDriver

A continuación, instalar Selenium dentro de tu proyecto:

npm install --save-dev selenium-webdriver

El uso de Selenium-web driver con Electron es el mismo que con sitios normales, excepto que tienes tiene que especificar manualmente como conectar el ChromeDriver y donde se encuentra el binario o ejecutable de Electron:

test.js
const webdriver = require('selenium-webdriver')
const driver = new webdriver.Builder()
// El "9515" es el puerto abierto por el ChromeDriver.
.usingServer('http://localhost:9515')
.withCapabilities({
'goog:chromeOptions': {
// Here is the path to your Electron binary.
binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
}
})
.forBrowser('chrome') // note: use .forBrowser('electron') for selenium-webdriver <= 3.6.0
.build()
driver.get('https://www.google.com')
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver')
driver.findElement(webdriver.By.name('btnG')).click()
driver.wait(() => {
return driver.getTitle().then((title) => {
return title === 'webdriver - Google Search'
})
}, 1000)
driver.quit()

Usando Playwright

Microsoft Playwright es un framework de pruebas de extremo a extremo construido usando protocolos de depuración remotos específicos del navegador, similar a la API Node.js de Puppeteer "headless" pero dirigida hacia pruebas de extremo a extremo. Playwright tiene soporte experimental de Electron a través del soporte de Electron para el Protocolo de Chrome DevTools (CDP).

Instala las dependencias

Puede instalar Playwright a través de su gestor de paquetes Node.js preferido. It comes with its own test runner, which is built for end-to-end testing:

npm install --save-dev @playwright/test

:::precaución Dependencias

This tutorial was written with @playwright/test@1.41.1. Check out Playwright's releases page to learn about changes that might affect the code below.

:::

Escribe tus tests

Playwright ejecuta tu aplicación en modo desarrollo a través de la API _electron.launch. Para apuntar esta API a su aplicación de Electron puede pasar la ruta al punto de entrada de su proceso principal (en este caso main.js).

const { test, _electron: electron } = require('@playwright/test')

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
// close app
await electronApp.close()
})

Después de eso, tendrás acceso a una instancia de la clase ElectronApp de Playwright. Esta es una clase poderosa que tiene acceso a módulos del proceso principal, por ejemplo:

const { test, _electron: electron } = require('@playwright/test')

test('get isPackaged', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// This runs in Electron's main process, parameter here is always
// the result of the require('electron') in the main app script.
return app.isPackaged
})
console.log(isPackaged) // false (porque estamos en modo de desarrollo)
// cerrar la aplicación
esperar electronApp.close()
})

It can also create individual Page objects from Electron BrowserWindow instances. Por ejemplo, para tomar la primera ventana del navegador y guardar una captura de pantalla:

const { test, _electron: electron } = require('@playwright/test')

test('save screenshot', async () => {
const electronApp = await electron.launch({ args: ['main.js'] })
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })
// close app
await electronApp.close()
})

Putting all this together using the Playwright test-runner, let's create a example.spec.js test file with a single test and assertion:

example.spec.js
const { test, expect, _electron: electron } = require('@playwright/test')

test('example test', async () => {
const electronApp = await electron.launch({ args: ['.'] })
const isPackaged = await electronApp. valuate(async ({ app }) => {
// Esto se ejecuta en el proceso principal de Electron, aquí siempre es
// el resultado del require('electron') en el script principal de la aplicación.
return app.isPackaged
})

expect(isPackaged).toBe(false)

// Wait for the first BrowserWindow to open
// and return its Page object
const window = await electronApp.firstWindow()
await window.screenshot({ path: 'intro.png' })

// close app
await electronApp.close()
})

Luego, ejecuta Playwright Test usando npx playwright test. Deberías ver como pasa la prueba, y tener una intro.png captura de pantalla en tu sistema de archivos.

☁  $ npx playwright test

Running 1 test using 1 worker

✓ example.spec.js:4:1 › example test (1s)
info

La prueba de Playwright ejecutará cualquier archivo que coincida con la expresión regular .*(test|spec)\.(js|ts|mjs). You can customize this match in the Playwright Test configuration options. It also works with TypeScript out of the box.

:::Leer más

Check out Playwright's documentation for the full Electron and ElectronApplication class APIs.

:::

Usar un controlador de prueba personalizado

También es posible escribir tu propio controlador personalizado usando el IPC-over-STDIO incorporado de node. js. Los controladores de prueba personalizados requieren que escribas un código de aplicación adicional, pero tienen una carga inferior y le permiten exponer métodos personalizados a su suite de pruebas.

Para crear un controlador personalizado, usaremos la API child_process de node. js. El conjunto de pruebas generará el proceso de Electron, luego establecerá un protocolo de mensajería simple:

testDriver.js
const childProcess = require('node:child_process')
const electronPath = require('electron')

// spawn the process
const env = { /* ... */ }
const stdio = ['inherit', 'inherit', 'inherit', 'ipc']
const appProcess = childProcess.spawn(electronPath, ['./app'], { stdio, env })

// listen for IPC messages from the app
appProcess.on('message', (msg) => {
// ...
})

// send an IPC message to the app
appProcess.send({ my: 'message' })

Dentro de la aplicación Electron, puedes escuchar mensajes y enviar respuestas usando la API de Node.js process:

main.js
// escucha mensajes de la suite de pruebas
process.on('message', (msg) => {
// ...
})

// envía un mensaje a la suite de pruebas
process.send({ my: 'message' })

Ahora podemos comunicar desde la suite de pruebas a la aplicación Electron usando el objeto appProcess.

Por conveniencia, es posible que desees encapsular appProcess en un objeto de controlador que ofrezca más funciones de alto nivel. Aquí hay un ejemplo de cómo puedes hacer esto. Empecemos por crear una clase de TestDriver:

testDriver.js
class TestDriver {
constructor ({ path, args, env }) {
this.rpcCalls = []

// start child process
env.APP_TEST_DRIVER = 1 // let the app know it should listen for messages
this.process = childProcess.spawn(path, args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })

// handle rpc responses
this.process.on('message', (message) => {
// pop the handler
const rpcCall = this.rpcCalls[message.msgId]
if (!rpcCall) return
this.rpcCalls[message.msgId] = null
// reject/resolve
if (message.reject) rpcCall.reject(message.reject)
else rpcCall.resolve(message.resolve)
})

// wait for ready
this.isReady = this.rpc('isReady').catch((err) => {
console.error('Application failed to start', err)
this.stop()
process.exit(1)
})
}

// simple RPC call
// to use: driver.rpc('method', 1, 2, 3).then(...)
async rpc (cmd, ...args) {
// send rpc request
const msgId = this.rpcCalls.length
this.process.send({ msgId, cmd, args })
return new Promise((resolve, reject) => this.rpcCalls.push({ resolve, reject }))
}

stop () {
this.process.kill()
}
}

module.exports = { TestDriver }

En el código de tu aplicación, puedes escribir un manejador simple para recibir llamadas RPC:

main.js
const METHODS = {
isReady () {
// hacer cualquier configuración necesaria
return true
}
// definir tus métodos RPC aquí
}

const onMessage = async ({ msgId, cmd, args }) => {
let method = METHODS[cmd]
if (! ethod) method = () => new Error('Método inválido: ' + cmd)
try {
const resolve = await method(. .args)
proceso. end({ msgId, resolve })
} catch (err) {
const reject = {
message: err.message,
stack: err.stack,
name: err.name
}
process. end({ msgId, reject })
}
}

if (process.env.APP_TEST_DRIVER) {
process.on('message', onMessage)
}

Luego, en tu suite de pruebas, puedes usar tu clase TestDriver con el framework de automatización de prueba de tu elección. El siguiente ejemplo usa ava, pero otras opciones populares como Jest o Mocha también funcionan:

test.js
const test = require('ava')
const electronPath = require('electron')
const { TestDriver } = require('. testDriver')

const app = new TestDriver({
path: electronPath,
args: ['. app'],
env: {
NODE_ENV: 'test'
}
})
test.before(async t => {
await app.isReady
})
test. fter.always('cleanup', async t => {
await app.stop()
})